using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

using Amazon.SimpleDB;
using AmazonModel = Amazon.SimpleDB.Model;
using Amazon.SimpleDB.Model;
using Amazon.SimpleDB.Mock;

using NSimpleDB.Service.Contract;

namespace NSimpleDB.Service.Amazon
{
    public class AmazonSimpleDBService : ISimpleDBService, ISimpleDBDashbord
    {
        #region global fields and ctor
        private const int DefaultMaxResultsetPageSize = 100;

        private AmazonSimpleDB sdb;
        private Dashboard.SimpleDBDashboard dashboard;


        public AmazonSimpleDBService() { }

        public AmazonSimpleDBService(string accessKeyId, string secretAccessKey)
        {
            this.Open(string.Format("accessKeyId={0};secretAccessKey={1}", accessKeyId, secretAccessKey));
        }

        public AmazonSimpleDBService(string connectionString)
        {
            this.Open(connectionString);
        }
        #endregion

        #region ISimpleDBService Members
        #region connection management
        public void Open(string connectionString)
        {
            this.Close();

            this.dashboard = new Dashboard.SimpleDBDashboard();
            this.sdb = this.CreateSimpleDBProxy(connectionString);
        }

        internal AmazonSimpleDB CreateSimpleDBProxy(string connectionString)
        {
            if (connectionString == null || connectionString == "")
                return new AmazonSimpleDBMock();
            else
            {
                Dictionary<string, string> args;
                args = this.AnalyseConnectionString(connectionString);
                return new AmazonSimpleDBClient(args["accessKeyId"], args["secretAccessKey"]);
            }
        }

        internal Dictionary<string, string> AnalyseConnectionString(string connectionString)
        {
            Dictionary<string, string> args = new Dictionary<string, string>();

            foreach (Match m in Regex.Matches(connectionString, @"(accessKeyId\s*=\s*(?<accessKeyId>[^\s,;]+)|secretAccessKey\s*=\s*(?<secretAccessKey>[^\s,;]+))(\s*(,|;|$)\s*)", RegexOptions.IgnoreCase))
            {
                if (m.Groups["accessKeyId"].Success)
                    args.Add("accessKeyId", m.Groups["accessKeyId"].Value);
                else if (m.Groups["secretAccessKey"].Success)
                    args.Add("secretAccessKey", m.Groups["secretAccessKey"].Value);
            }

            if (!args.ContainsKey("accessKeyId") || args["accessKeyId"] == "")
                throw new SimpleDBException(100, "Cannot connect to Amazon SimpleDB! Access key id is missing.");
            if (!args.ContainsKey("secretAccessKey") || args["secretAccessKey"] == "")
                throw new SimpleDBException(101, "Cannot connect to Amazon SimpleDB! Secret access key is missing.");

            return args;
        }


        public void Close()
        {
            this.sdb = null;
        }
        #endregion

        #region domain management
        public void CreateDomain(string domainName)
        {
            try
            {
                CreateDomain cd = new CreateDomain();
                cd.DomainName = domainName;
                CreateDomainResponse cdr = this.sdb.CreateDomain(cd);

                if (cdr.IsSetResponseMetadata())
                    this.dashboard.SetResponseMetaData(cdr.ResponseMetadata.RequestId, cdr.ResponseMetadata.BoxUsage);
                else
                    this.dashboard.SetResponseMetaData("", "");
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("CreateDomain: SimpleDB failed to create domain '{0}'! Service exception {1}: {2}", domainName, ex.ErrorCode, ex.Message), ex);
            }
        }


        public void DeleteDomain(string domainName)
        {
            try
            {
                DeleteDomain dd = new DeleteDomain();
                dd.DomainName = domainName;
                DeleteDomainResponse ddr = this.sdb.DeleteDomain(dd);
                if (ddr.IsSetResponseMetadata())
                    this.dashboard.SetResponseMetaData(ddr.ResponseMetadata.RequestId, ddr.ResponseMetadata.BoxUsage);
                else
                    this.dashboard.SetResponseMetaData("", "");
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("DeleteDomain: SimpleDB failed to delete domain '{0}'! Service exception {1}: {2}", domainName, ex.ErrorCode, ex.Message), ex);
            }
        }


        public string[] ListDomains()
        {
            List<string> completeResultset = new List<string>();

            string nextToken = null;
            do
            {
                string[] resultsetPage = this.ListDomains(ref nextToken);
                completeResultset.AddRange(resultsetPage);
            } while (nextToken != null);

            return completeResultset.ToArray();
        }

        public string[] ListDomains(ref string nextToken)
        {
            return this.ListDomains(AmazonSimpleDBService.DefaultMaxResultsetPageSize, ref nextToken);
        }

        public string[] ListDomains(int maxNumberOfDomains, ref string nextToken)
        {
            ListDomainsResponse ldr;
            try
            {
                ListDomains ld = new ListDomains();
                ld.MaxNumberOfDomains = maxNumberOfDomains;
                ld.NextToken = nextToken;
                ldr = this.sdb.ListDomains(ld);
                if (ldr.IsSetResponseMetadata())
                    this.dashboard.SetResponseMetaData(ldr.ResponseMetadata.RequestId, ldr.ResponseMetadata.BoxUsage);
                else
                    this.dashboard.SetResponseMetaData("", "");
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("ListDomains: SimpleDB failed to retrieve domains! Service exception {0}: {1}", ex.ErrorCode, ex.Message), ex);
            }

            if (ldr.IsSetListDomainsResult())
            {
                nextToken = ldr.ListDomainsResult.NextToken;
                return ldr.ListDomainsResult.DomainName.ToArray();
            }
            else
                throw new SimpleDBException("ListDomains: No domains were returned by the service!");
        }
        #endregion

        #region item management
        public void PutAttributes(string domainName, string itemName, params ISimpleDBAttribute[] attributes)
        {
            try
            {
                PutAttributes pa = new PutAttributes();
                pa.DomainName = domainName;
                pa.ItemName = itemName;
                foreach (ISimpleDBAttribute sa in attributes)
                {
                    ReplaceableAttribute ra = new ReplaceableAttribute();
                    ra.Name = sa.Name;
                    ra.Value = sa.Value;
                    if (sa is ISimpleDBAttributeToPut)
                        ra.Replace = (sa as ISimpleDBAttributeToPut).Replace;
                    pa.Attribute.Add(ra);
                }
                PutAttributesResponse par = this.sdb.PutAttributes(pa);

                if (par.IsSetResponseMetadata())
                    this.dashboard.SetResponseMetaData(par.ResponseMetadata.RequestId, par.ResponseMetadata.BoxUsage);
                else
                    this.dashboard.SetResponseMetaData("", "");
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("PutAttributes: SimpleDB failed to put attributes into item '{0}' in domain '{1}'! Service exception {2}: {3}", itemName, domainName, ex.ErrorCode, ex.Message), ex);
            }
        }


        public ISimpleDBAttribute[] GetAttributes(string domainName, string itemName, params string[] attributeNames)
        {
            try
            {
                GetAttributes ga = new GetAttributes();
                ga.DomainName = domainName;
                ga.ItemName = itemName;
                ga.AttributeName = new List<string>(attributeNames);
                GetAttributesResponse gar = this.sdb.GetAttributes(ga);

                if (gar.IsSetResponseMetadata())
                {
                    this.dashboard.SetResponseMetaData(gar.ResponseMetadata.RequestId, gar.ResponseMetadata.BoxUsage);

                    List<ISimpleDBAttribute> attributes = new List<ISimpleDBAttribute>();
                    foreach (AmazonModel.Attribute a in gar.GetAttributesResult.Attribute)
                        attributes.Add(new SimpleDBAttribute(a.Name, a.Value));
                    return attributes.ToArray();
                }
                else
                {
                    this.dashboard.SetResponseMetaData("", "");
                    return new ISimpleDBAttribute[] { };
                }
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("GetAttributes: SimpleDB failed to get attributes from item '{0}' in domain '{1}'! Service exception {2}: {3}", itemName, domainName, ex.ErrorCode, ex.Message), ex);
            }
        }


        public void DeleteAttributes(string domainName, string itemName)
        {
            this.DeleteAttributes(domainName, itemName, new string[] { });
        }

        public void DeleteAttributes(string domainName, string itemName, params string[] attributeNames)
        {
            List<ISimpleDBAttribute> attributes = new List<ISimpleDBAttribute>();
            foreach (string an in attributeNames)
                attributes.Add(new SimpleDBAttribute(an, null));
            this.DeleteAttributes(domainName, itemName, attributes.ToArray());
        }

        public void DeleteAttributes(string domainName, string itemName, params ISimpleDBAttribute[] attributes)
        {
            try
            {
                DeleteAttributes da = new DeleteAttributes();
                da.DomainName = domainName;
                da.ItemName = itemName;
                foreach (ISimpleDBAttribute sa in attributes)
                {
                    AmazonModel.Attribute a = new global::Amazon.SimpleDB.Model.Attribute();
                    a.Name = sa.Name;
                    a.Value = sa.Value;
                    da.Attribute.Add(a);
                }
                DeleteAttributesResponse dar = this.sdb.DeleteAttributes(da);

                if (dar.IsSetResponseMetadata())
                    this.dashboard.SetResponseMetaData(dar.ResponseMetadata.RequestId, dar.ResponseMetadata.BoxUsage);
                else
                    this.dashboard.SetResponseMetaData("", "");
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("DeleteAttributes: SimpleDB failed to delete attributes from item '{0}' in domain '{1}'! Service exception {2}: {3}", itemName, domainName, ex.ErrorCode, ex.Message), ex);
            }
        }


        public string[] Query(string domainName, string queryExpression)
        {
            List<string> completeResultset = new List<string>();

            string nextToken = null;
            do
            {
                string[] resultsetPage = this.Query(domainName, 
                                                    queryExpression, 
                                                    AmazonSimpleDBService.DefaultMaxResultsetPageSize, 
                                                    ref nextToken);
                completeResultset.AddRange(resultsetPage);
            } while (nextToken != null);

            return completeResultset.ToArray();
        }

        public string[] Query(string domainName, string queryExpression, ref string nextToken)
        {
            return this.Query(domainName, queryExpression, AmazonSimpleDBService.DefaultMaxResultsetPageSize, ref nextToken);
        }

        public string[] Query(string domainName, string queryExpression, int maxNumberOfItems, ref string nextToken)
        {
            QueryResponse qar;

            try
            {
                Query qa = new Query();
                qa.DomainName = domainName;
                qa.QueryExpression = queryExpression;
                qa.MaxNumberOfItems = maxNumberOfItems;
                qa.NextToken = nextToken;
                qar = this.sdb.Query(qa);

                if (qar.IsSetResponseMetadata())
                    this.dashboard.SetResponseMetaData(qar.ResponseMetadata.RequestId, qar.ResponseMetadata.BoxUsage);
                else
                    this.dashboard.SetResponseMetaData("", "");
            }
            catch (AmazonSimpleDBException ex)
            {
                this.dashboard.SetExceptionData(ex);
                throw new SimpleDBException(string.Format("Query: SimpleDB failed to execute query '{0}' on domain '{1}'! Service exception {2}: {3}", queryExpression, domainName, ex.ErrorCode, ex.Message), ex);
            }

            if (qar.IsSetQueryResult())
            {
                nextToken = qar.QueryResult.NextToken;
                return qar.QueryResult.ItemName.ToArray();
            }
            else
                throw new SimpleDBException("Query: No query result was generated!");
        }
        #endregion
        #endregion


#if DEBUG
        internal List<string> RawQuery(string domainName, string query, int maxNumberOfItems, ref string nextToken)
        {
            QueryResponse qar;
            Query qa = new Query();
            qa.DomainName = domainName;
            qa.QueryExpression = query;
            qa.MaxNumberOfItems = maxNumberOfItems;
            qa.NextToken = nextToken;
            qar = this.sdb.Query(qa);

            nextToken = qar.QueryResult.NextToken;
            return qar.QueryResult.ItemName;
        }
#endif


        #region ISimpleDBDashbord Members
        public ISimpleDBResponseMetaData LastResponseMetaData
        {
            get { return this.dashboard.GetResponseMetaData(); }
        }

        public ISimpleDBExceptionData LastExceptionData
        {
            get { return this.dashboard.GetExceptionData(); }
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this.Close();
        }

        #endregion
    }
}
